Assembly Language
© Copyright Brian Brown, 1988-2000. All rights reserved.
| Notes | Home Page |


REAL TIME EXECUTIVES

OVERVIEW
This article deals with the development of a real-time executive for PC applications. The executive developed is based on a pre-emptive round-robin queue, and supports multiple tasks within a global programs address/data space.

A large memory model Turbo C program is split into a number of independent or co-operating tasks, each task being inserted into a round robin queue. The processors real-time clock is redirected to a scheduler, part of the executive, which pre-empts the running process and switches to a waiting task.

The executive supplies system calls for suspending and waking up a task, as well as task intercommunication via messages.

The executive was developed as a teaching aid to software engineering students. As such, it illustrates the operation of the kernel at first hand, allowing students to modify and test its behavior. A simple kernel was essential, which permits student familiarization quickly and efficiently.

Initially, a co-operative scheduler is developed. This scheduler is easy to implement as each task voluntarily releases the processor. Later, the scheduler is changed to a pre-emptive version, which allows simultaneous running tasks, with a block/wakeup message passing system.


Task Scheduling, Priority, Suspension and Wakeup
Scheduling refers to deciding which waiting task should be run next by the processor when it becomes free. This implies that each task being executed by the processor has a task state associated with it.

Other task states will be added later. A processor is switched from one task to another either voluntarily or by pre-emption. To restart a task which has yielded the processor, the information needed is,

Often, a task is required to execute more often than other tasks in the system (ie, receive more processor time). This means each task should have a task priority. When the decision is being made concerning the next task to run, higher priority tasks will be considered first. This means that a tasks priority could dynamically change throughout its lifetime.

A simple scheduling system is the round-robin queue, where each task has equal priority and are inserted into a queue. The task at the front of the queue receives the processor when it becomes free. A task releasing the processor enters the rear of the queue, gradually working its way to the top.

Often, a task will perform some function (like a compile or print operation) which relies upon the functions completion before it can continue. In this case, rather than waste processor time executing a task which cannot continue, the task state is marked as blocked, and the task is not considered when the processor becomes available.

This can be done by removing the task from the processor queue, or modifying the scheduling algorithm so that tasks flagged as blocked are not considered for assignment to the processor. Blocking a task is often called task suspension.

When the event upon which a blocked task is waiting for occurs (eg, printer has finished), its task state is changed to ready and it is reinserted into the ready queue. This is referred to as task wakeup.

Once a new task has been decided upon, the processor must be made to run the selected task. This is called dispatching a task. The processor registers are updated to reflect the new task. The act of switching from one task to another is called context switching.

To manage each task in a computer system, the kernel keeps a table for each task. This is called a process control block (PCB). Its definition in C could look like,


	struct task  {
	   unsigned int SS;
	   unsigned int SP;
	   unsigned int process_state;
	   unsigned int process_priority;
	} PROCESS[NUM_TASKS];

Each task in the computer system also has its own stack space, so these definitions look like,


	#define STACK_LEN   1025
	unsigned int stack_main[STACK_LEN];
	unsigned int stack_task1[STACK_LEN];
	unsigned int stack_task2[STACK_LEN];

In a multi-user system, the memory for the tasks code, data area and stack space would be allocated by the memory management functions of the kernel, rather than globally declared (which occurs in this example and small control type systems).

The kernel requires some extra information to keep track of the current task.


	int task_number;

Consider the arrival of a new task in the system. It is allocated memory to run, added to the ready queue and its PCB filled in. In this example, the ready queue is an array of PCB's, one entry per task (maximum of three tasks). A function which performs this looks like,


	void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)())
	{
	   /* insert task details into task PROCESS table */
	   PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace );
	   PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN-2]);
	   PROCESS[tasknum].process_state = READY;
	   PROCESS[tasknum].process_priority = NORMAL_PRIORITY;
	   stackspace[STACK_LEN-2] = (unsigned int) FP_OFF( fptr );
	   stackspace[STACK_LEN-1] = (unsigned int) FP_SEG( fptr );
	   /* contained at the top of the stack is the entry address into the task */
	}

This function inserts the pointer to the tasks stack space into the PROCESS table, creating a PCB entry for the task. It accepts the entry point to the task, a pointer to its stack space, and the entry slot into the PROCESS array.

It loads the tasks PCB stack registers with the address of the stack space for the task, and inserts the entry address of the task onto the top of the stack for that task. This means that a RET instruction will execute this task if the processors stack registers are altered to point to the tasks stack space!

The kernel inserts the new tasks with a function call which looks like,


	newprocess( 0, stack_main,  &main  );
	newprocess( 1, stack_task1, &task1 );
	newprocess( 2, stack_task2, &task2 );

In a non-pre-emptive scheduling system, a task releases the processor voluntarily. It does this by calling a kernel function. We have called this function transfer, which switches to another ready task. Its purpose is to save the current tasks stack registers, and select the next task for execution. Having selected a task, it then switches the processors stack registers to point to the tasks stack space, and executes a RETurn instruction.

It changes the releasing tasks state from RUNNING to READY, and updates the new tasks state from READY to RUNNING. Inside the function, the scheduler skips tasks which are blocked.


	void far transfer( void )
	{
	   disable();
	   /* save current running task */
	   PROCESS[task_number].SS = (unsigned int) _SS;
	   PROCESS[task_number].SP = (unsigned int) _SP;
	   if( PROCESS[task_number].process_state == RUNNING)
	     PROCESS[task_number].process_state = READY;
	   /* set up for new task */
	   task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;
	   while(PROCESS[task_number].process_state == BLOCKED)
	       task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;

	   _SS = PROCESS[task_number].SS;
	   /* _SP = PROCESS[task_number].SP; 
	   This statement must be done is assembler else the
	   Compiler will try to use the stack to generate a valid _ES reference.
	   As SS has been adjusted, BUT NOT SP, this goes into nomans land. */
	   asm   mov   sp, word ptr es:[bx+2];
	   /* PROCESS[task_number].process_state = RUNNING; */
	   asm   mov   word ptr es:[bx+4], RUNNING;
	   enable();
	}

Please note the need for assembler within the scheduler to adjust the stack pointer to the new task. It is important not to change the order of the fields within the structure PROCESS, as it assumes that the SP value is located at offset 2 and the task state at offset 4.

The only remaining functions handle task suspension and wakeup. A task is suspended using block, whilst a task is resumed using wakeup. These functions change the calling tasks state, though in multi-user systems would also involve the use of secondary storage media storing blocked tasks on disk rather than in main memory).

A blocked task is woken up by another task or the kernel. Because it does not receive processor time, it cannot wake itself up.


	void block(void)
	{
	   PROCESS[task_number].process_state = BLOCKED;
	   transfer();
	}

	void wakeup(int task_num)
	{
	   PROCESS[task_num].process_state = READY;
	}

One other requirement is for tasks to sleep for a specified period. A kernel function called sleep provides this, which accepts a tickrate as a parameter. This changes the calling tasks state to SLEEPING. This necessitates a small change to the definition of a tasks PCB.


	#define SLEEPING    3

	struct task  {
	   unsigned int SS;
	   unsigned int SP;
	   unsigned int process_state;
	   unsigned int process_priority;
	   unsigned int sleep_rate;
	} PROCESS[NUM_TASKS];

The newprocess function has the following line added to initialize the sleep_rate variable for each task.


	void newprocess( .... )
	{
	   ......
	   PROCESS[task_number].sleep_rate = 0;
	   ......
	}

The transfer function is altered to take into account sleeping tasks, by decrementing their sleep_rate count each time its called. The problem with this co-operative implementation is that it really does not work, since a task might never call transfer and release the processor. However, when the code is changed to be pre-emptive, this problem disappears.


	#define PTNPS   PROCESS[task_number].process_state

	void far transfer( void )
	{
	   disable();
	   ......   /* save current running task */
	   /* set up for new task */
	   task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;
	   while( (PTNPS == BLOCKED) || (PTNPS == SLEEPING ) )
	   {
	      if( PTNPS == SLEEPING)
	         if( (PROCESS[task_number].sleep_rate--) == 0) PTNPS = READY;
	      task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;
	   }
	   ......
	}

A running task calls the sleep function to delay itself. This changes its process state to sleeping and initiates a transfer to another ready task. The sleep function looks like


	void sleep( int delay )
	{
	   PROCESS[task_number].process_state = SLEEPING;
	   PROCESS[task_number].sleep_rate = delay;
	   transfer();
	}


Task Intercommunication
Tasks intercommunicate by sending messages, data or code between them. Thus, each task has a receive buffer and some task flag associated with sending and receiving of messages.

The following addition is made to the definition of a tasks PCB.


	struct task  {
	  .......
	  int msg;
	  char far *msgbuf;
	} PROCESS[NUM_TASKS];

The following addition is made to function newprocess.


	void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)(), char far *msgbuffer)
	{
	   ......
	   ......
	   PROCESS[tasknum].msg = FALSE;
	   PROCESS[tasknum].msgbuf = msgbuffer;
	}

Sendmessage is a kernel function that accepts a destination task_number and a pointer to the message being sent. It copies the message into the receiving tasks message buffer and signals the receiving task that a message has arrived.


	#define MAXMSGLEN  80

	void far sendmessage( int task_number, char far *msg )
	{
	   int count = 0;
	   char far *rbuf;
	   disable();                                            /* don't interrupt this     */
	   rbuf = PTN(msgbuf);                           /* address of tasks recbuf  */
	   while( (*msg) && (count < MAXMSGLEN)) {       /* while message is not null*/
	      count++;                                          /* and count is less than   */
	      *rbuf = *msg;                                   /* the size of a recbuffer  */
	      rbuf++; msg++;                                /* copy from sender to      */
	   }                                                          /* receiver                 */
	   *rbuf = '\0';                                          /* NULL terminate recbuffer */
	   PTN(msg) = TRUE;                              /* signal message arrival   */
	   enable();                                              /* re-enable interrupts now */
	}

receivemessage is a kernel function that a running task calls to receives a message. If no message has arrived, the task waits (is blocked) till the message arrives. Upon receipt of the message, the function returns with the data in the receive buffer for the calling task.


	char far *receivemessage(void)
	{
	   if( PROCESS[task_number].msg == FALSE ) block();
	   return PROCESS[task_number].msgbuf;
	}

The following change is made to process transfer to allow messages and the unblocking of tasks waiting for a message.


	#define PTNPS   PROCESS[task_number].process_state

	void far transfer(void)
	{
	   disable();
	   ......   /* save current running task */
	   /* set up for new task */
	   task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;
	   while( (PTNPS == BLOCKED) || (PTNPS == SLEEPING ) )
	   {
	      if( PTNPS == SLEEPING)
	         if( (PROCESS[task_number].sleep_rate--) == 0) PTNPS = READY;
	      if( (PTNPS == BLOCKED) && (PROCESS[task_number].msg == TRUE) )
	         PTNPS = READY;
	      task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0;
	   }
	   PROCESS[task_number].process_state = RUNNING;
	   ......
	}


Task Synchronization
The block, wakeup, sleep, sendmessage and receivemessage functions can be used by tasks to synchronize their activity.


CHANGING TO A PRE-EMPTIVE SCHEDULER
Developing a pre-emptive version is not that difficult, since much of the ground work has been correctly done. The scheduler must be interrupt driven, so we will hook it into the real time clock (RTC) of the PC which is executed 18.2 times a second.

Changing the function transfer to an interrupt type and placing its address into the interrupt vector associated with the RTC are the essential changes.

When an interrupt occurs, all the processor registers except SS and SP are saved. The scheduler will terminate by swapping the stack, but instead of using a return statement, it will execute a return from interrupt instruction. This will unstack all the registers. The newprocess function will need to simulate an interrupt return instruction on the top of each tasks stack frame. The necessity of saving the BP register within the PCB of a task is now eliminated. This was necessary before to keep track of each tasks local variables, which are accessed via the BP register.

A global variable is used to keep the address stored in the old RTC vector, as this will be restored when exiting to DOS. Failure to restore this will result in a reboot.

The RTC vector is saved, and then the scheduler address is inserted. This must be done with interrupts disabled, to prevent the scheduler becoming active before the kernel has a chance to set up everything. This vector is altered once all the tasks have been initialised by calls to new_process().


	void far interrupt (*oldtimer)();

	oldtimer = getvect( 0x08 );		/* save old RTC interrupt		*/
	disable();				/* stop interrupts		*/
	setuptimer( transfer );		/* hook scheduler into RTC		*/
	enable();				/* and now its away		*/

Upon exit to DOS, the system must be restored to normal. This is achieved by either the function ctc or myexit.


	disable();				/* turn off interrupts		*/
	setvect(  0x08, oldtimer );		/* restore original timer		*/
	outportb( 0x43, 0x36 );		/* reset 8253 channel 0 to mode 3 */
	outportb( 0x40, 0x00 );
	outportb( 0x40, 0x00 );
	enable();				/* reenable interrupts		*/

The scheduler is changed to type interrupt. This terminates the function with an IRET instruction. Before the function terminates, the last statement resets the 8259 Priority Interrupt Controller (PIC) chip to allow further interrupts. If this is not done, the scheduler is invoked only once. Writing a value of 0x20 to the PIC permits further interrupts to be processed.


	#define PIC		0x20	/* Priority Interrupter Controller address */
	#define EOI		0x20	/* End Of Interrupt signal code for PIC */

	void far interrupt transfer(void)
	{
	  .....
	  outportb( PIC, EOI );
	}

The function newprocess is altered to create a dummy interrupt state on the top of the tasks stack space. This is necessary, as the scheduler switches to a tasks stack space, then executes an IRET statement.


	void far newprocess( int task_number, unsigned int stack[], void far (*fptr)(), char far *msgbuffer )
	{
	   .....
	   stack[STACK_LEN - 2] = (unsigned int) 0x0200;  /* insert a dummy flag value, interrupts enabled */
	   stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr );  /* place CS:IP of task into tasks stack space */
	   stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr );
	   stack[STACK_LEN - 5] = _AX;
	   stack[STACK_LEN - 6] = _BX;
	   stack[STACK_LEN - 7] = _CX;
	   stack[STACK_LEN - 8] = _DX;
	   stack[STACK_LEN - 9] = _DS;   /* _ES */
	   stack[STACK_LEN -10] = _DS;   /* _DS */
	   stack[STACK_LEN -11] = _SI;
	   stack[STACK_LEN -12] = _DI;
	   stack[STACK_LEN -13] = _BP;
	}

The only other changes are necessary to the block, wakeup, sleep and sendmessage functions. Because the scheduler can interrupt at any time, code which alters the state of any PROCESS fields should do so with interrupts disabled. Thus these functions begin with the statement disable and end with enable. The functions block and sleep must invoke the scheduler via an interrupt call, they do this by waiting for an interrupt to occur (using the HLT instruction).


	void far sleep(unsigned int clockticks)
	{
	   disable();
	   ......
	   enable();
	   asm hlt;    /* interrupt call to transfer();     */
	}

One final function is called setuptimer, which inserts the address of the scheduler into the RTC interrupt vector. It also reprograms channel 0 of the 8253 to interrupt at a rate of 72 times per second (0x4000). The scheduler frequency is easily altered by changing the last two bytes output to the 8253, which are the LSB and MSB of a divisor. Values of 0x0000 will generate a frequency rate of 18.2 times per second, a divisor of 0x8000 will generate a rate of 144 times per second.


	void far setuptimer( void interrupt far (*switcher)() )
	{
	   setvect(  0x08, switcher ); /* link into RTC interrupt 0x08 */
	   outportb( 0x43, 0x34 );    /* reprogram 8253, mode 2 channel 0 */
	   outportb( 0x40, 0x00 );
	   outportb( 0x40, 0x40 );
	}

Taking over the RTC interrupt causes two things. Firstly, the DOS time of day will not function, and the motor time out for floppy disk drives will not work.

The pre-emptive scheduler has a serious restriction. Since DOS is not re-entrant, each task must not call DOS. It is possible for one task to use a particular DOS call, then be interrupted by another task trying to use the same DOS call. If this occurs, the system will hang. As the executive is designed for students to create NON-DOS dependant programs (control, data logging, embedded systems), this restrictions is acceptable.

The overhead of the scheduler was timed at 22 microseconds on a 386 running at 20mhz. With a switching rate of 18.2 times per seond, this means each task runs for approximately 54.89 milliseconds before being interrupted. This gives a system overhead of 4 percent.

Listing TWO contains the code for the pre-emptive scheduler, including task suspension, wakeup and message passing functions.


A RUDIMENTARY DOS PRE-EMPTIVE SCHEDULER
So far, we have covered two basic schedulers, co-operative and pre-emptive. The pre-emptive version is effecient and simple. Its only drawback is the inability of tasks to use DOS type functions. We shall now convert the scheduler to support DOS video functions.

We need some method of telling the scheduler not to switch tasks when it discovers the task just interrupted was executing a DOS call. In this way, the task will not be switched, and will continue till the DOS call is finished.

To accomplish this, two flags are used. The flags are


	InVideoBios
	INDOS

InVideoBios keeps track of calls to the INT10h vector which handles ROMBIOS calls to the video screen. It is handled by a function which is inserted into the int10 vector, replacing the system one. This function looks like


	void interrupt far newint10( void )
	{
	   ++InVideoBios;
	   (*oldint10)();
	   --InVideoBios;
	   outportb( PIC, EOI);
	}

When a task calls the int10 routines (via DOS), the InVideoBios flag is incremented, and control is passed to the original vector. Upon return, the flag is decremented and the Priority Interrupt Controller (PIC) is reset to allow further processing.

The other flag is a system variable kept by DOS to indicate to itself when system calls are in progress. The address of this variable is obtained using a system call to DOS of type 0x34.


	   _AH = 0x34;                   /* get pointer to INDOS flag */
	   geninterrupt( 0x21 );
	   INDOS = MK_FP( _ES, _BX );

The scheduler is altered to prevent switching from a task currently in DOS.


	void far interrupt scheduler( void )
	{
	   if( *INDOS || InVideoBios ) {  /* don't switch if in DOS */
	      outportb( PIC, EOI );
	      return;                     /* go back to interrupted task */
	   }
	   .....
	   .....
	}

These changes now allow a task to use the printf statement. Listing THREE is a code example of the DOS pre-emptive scheduler.

It should be noted that to be bullet proof, the DOS scheduler should also take over other interrupts which might cause problems (keyboard, disk access). We have shown how to get it running using video calls, the rest is up to you.


SOFTWARE LISTINGS

Listing 1
Co-operative scheduler

Listing 2
Pre-emptive scheduler

Listing 4
Embedded system for PC-DIO card/panel using pre-emptive scheduler



LISTING 1  task.c   a co-operative scheduler for the PC


/* task1.c, co-operative round-robin scheduler
   Copyright Brian Brown, CIT, 28th September 1989
   All rights reserved.

   To compile this program type,
tcc -N- -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task1.c

   each task has its own stack space
   each tasks information is stored into PROCESS using newprocess()
   a task transfers to another by calling transfer()
   each task can use DOS routines
   each task can use local variables
*/

#include <stdio.h>
#include <dos.h>

#define NUM_TASKS	3
#define STACK_LEN     	1025
#define READY       		0x0000
#define RUNNING    	0x0001
#define BLOCKED     	0x0002

extern unsigned _stklen = 8192;

struct task {
   unsigned int SS;
   unsigned int SP;
   unsigned int BP;
   unsigned int process_state;
} PROCESS[NUM_TASKS];

unsigned int stack_main[STACK_LEN];
unsigned int stack_task1[STACK_LEN];
unsigned int stack_task2[STACK_LEN];
int task_number;

void far transfer( void )
{
   disable();
   PROCESS[task_number].SS = (unsigned int) _SS;   /* save current running task */
   PROCESS[task_number].SP = (unsigned int) _SP;
   PROCESS[task_number].BP = (unsigned int) _BP;
   PROCESS[task_number].process_state = READY;
   /* set up for new task */
   task_number++;
   if( task_number > (NUM_TASKS-1) ) task_number = 0;
   _SS = PROCESS[task_number].SS;
   /* _SP = PROCESS[task_number].SP;
      This statement must be done is assembler else the C Compiler will try to 
      use the stack to generate a valid _ES
      reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land.            */
   asm   mov   sp, word ptr es:[bx+2];       /* _SP = PROCESS[task_number].SP                */
   asm   mov   bp, word ptr es:[bx+4];       /* _BP = PROCESS[task_number].BP                */
   asm   mov   word ptr es:[bx+6], RUNNING;
   /* PROCESS[task_number].process_state = RUNNING */
   enable();
}

void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)())
{
   /* insert task details into task PROCESS table */
   PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace );
   PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN-2]);
   PROCESS[tasknum].process_state = READY;
   stackspace[STACK_LEN-2] = (unsigned int) FP_OFF( fptr );
   stackspace[STACK_LEN-1] = (unsigned int) FP_SEG( fptr );
   /* contained at the top of the stack is the entry address into the task */
}

void far task1( void )
{
   int i;
   i = 0;
   while( 1 ) {
      printf("Task1: i = %d\n", i);
      i += 1;
      transfer();
   }
}

void far task2( void )
{
   int i;
   i = 0;
   while( 1 ) {
      printf("Task2: i = %d\n", i);
      i += 2;
      transfer();
   }
}

void far main()
{
   int i;
   i = 0;
   task_number=0;

   /* initialise the task PROCESS table */
   newprocess( 0, stack_main, &main );
   newprocess( 1, stack_task1, &task1 );
   newprocess( 2, stack_task2, &task2 );

   while( 1 ) {
      printf("main: i = %d\n", i);
      i += 3;
      transfer();
   }
}





LISTING 2  task2.c  a pre-emptive scheduler for PC


/* task.c, interrupt driven round robin scheduler
   Copyright Brian Brown, CIT, 28th September 1989
   All rights reserved.

   To compile this program type,
tcc -N- -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task2.c

   each task MUST NOT call DOS,   each task is treated equally
*/

#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include <alloc.h>

#define TRUE             1
#define FALSE            0
#define NUM_TASKS        3              /* number of tasks in system 0,1,2   */
#define READY            0              /* waiting for the processor         */
#define RUNNING          1              /* currently has the processor       */
#define BLOCKED          2              /* waiting for message               */
#define SLEEPING         3              /* asleep, cannot run                */
#define NORMAL_PRIORITY  7
#define HIGH_PRIORITY    1
#define LOW_PRIORITY    14
#define STACK_LEN       1025            /* length of stack for a task        */
#define F1              0x3b            /* function key F1 code              */
#define PIC             0x20            /* Priority Interrupt controller     */
#define EOI             0x20            /* end of interrupt for PIC          */
#define MAXMSGLEN       80              /* maximum length of a message       */
#define PTN(x)          PROCESS[task_number].x

struct task {                           /* process control block for tasks   */
   unsigned int SS;                     /* stack segment register            */
   unsigned int SP;                     /* stack pointer for each task       */
   unsigned int process_state;          /* task state                        */
   unsigned int process_priority;       /* task priority                     */
   unsigned int sleep_rate;             /* task delay time in ticks          */
   int msg;                             /* true if message arrived           */
   char far *msgbuf;                    /* pointer to message space          */
   unsigned int clock_ticks;            /* number of timeslices per task     */
} PROCESS[NUM_TASKS];                   /* one entry per known task          */

extern unsigned _stklen = 8192;

unsigned int stack_main[STACK_LEN];     /* stack for main task               */
unsigned int stack_task1[STACK_LEN];    /* stack for task1                   */
unsigned int stack_task2[STACK_LEN];    /* stack for task2                   */
unsigned char msg_main[MAXMSGLEN+1];    /* message buffer for main task      */
unsigned char msg_task1[MAXMSGLEN+1];   /* message buffer for task1          */
unsigned char msg_task2[MAXMSGLEN+1];   /* message buffer for task2          */

unsigned int task_number;               /* holds current running task ID     */
void far interrupt (*oldtimer)();           /* holds old int8h vector            */
unsigned int x,y,offset;                     /* used by print routine             */

void far sleep(unsigned int clockticks) /* voluntary task sleep              */
{
   disable();                           /* turn off interrupts               */
   PTN(process_state) = SLEEPING;       /* change task state to SLEEPING     */
   PTN(sleep_rate) = clockticks;        /* fill in duration to sleep         */
   enable();                            /* re-enable interrupts              */
   asm hlt;                             /* interrupt call to transfer();     */
}

void far block( void )                  /* block a task                      */
{
   disable();                           /* turn off interrupts               */
   PTN(process_state) = BLOCKED;        /* change task state                 */
   enable();                            /* re-enable interrupts              */
   asm hlt;                             /* wait for transfer                 */
}

void far wakeup( int task_number )      /* unblock a task                    */
{
   disable();                           /* turn off interrupts               */
   PTN(process_state) = READY;          /* change task state                 */
   enable();                            /* re-enable interrupts              */
}

void far sendmessage( int task_number, char far *msg )
{
   int count = 0;
   char far *rbuf;
   disable();                                    /* don't interrupt this     */
   rbuf = PTN(msgbuf);                           /* address of tasks recbuf  */
   while( (*msg) && (count < MAXMSGLEN)) {       /* while message is not null*/
      count++;                                   /* and count is less than   */
      *rbuf = *msg;                              /* the size of a recbuffer  */
      rbuf++; msg++;                             /* copy from sender to      */
   }                                             /* receiver                 */
   *rbuf = '\0';                                 /* NULL terminate recbuffer */
   PTN(msg) = TRUE;                              /* signal message arrival   */
   enable();                                     /* re-enable interrupts now */
}

char far * recievemessage( void )
{
   if( PTN(msg) == FALSE ) block();              /* wait for message arrival */
   return PTN(msgbuf);                           /* return pointer to buffer */
}

void far setuptimer( void interrupt far (*switcher)() )
{
   setvect(  0x08, switcher );          /* link into RTC interrupt 0x08      */
   outportb( 0x43, 0x34 );              /* reprogram 8253, mode 2 channel 0  */
   outportb( 0x40, 0x00 );
   outportb( 0x40, 0x40 );
}

void far interrupt transfer( void )
{
   PTN(SS) = (unsigned int) _SS;  /* save current running task stack pointers*/
   PTN(SP) = (unsigned int) _SP;
   if( PTN(process_state) == RUNNING)   /* if a running task, change to ready*/
     PTN(process_state) = READY;
   task_number++;                       /* select next task to run           */
   if( task_number > (NUM_TASKS-1) )  task_number = 0;
   /* skip tasks which are blocked and sleeping. for sleeping tasks decrement
      their sleep_rate count, and if zero, alter their task state to READY.
      For blocked tasks, see if they have recieved a message, and if they
      have, change their task state to READY                                 */
   while((PTN(process_state) == BLOCKED)||(PTN(process_state) == SLEEPING)) {
      if( PTN(process_state) == SLEEPING) {
         PTN(sleep_rate--);
         if( PTN(sleep_rate) == 0)  wakeup( task_number );
      }
      if((PTN(process_state) == BLOCKED) && (PTN(msg) == TRUE))  {
         PTN(process_state) = READY;
         PTN(msg) = FALSE;
      }
      task_number++;
      if( task_number > (NUM_TASKS-1) ) task_number = 0;
   }
   _SS = PTN(SS);               /* swap to stack of newly selected task      */
   /* _SP = PTN(SP);            This statement must be done is assembler else
                                the C Compiler will try to use the stack to
                                generate a valid _ES reference. As SS has been
                                adjusted, BUT NOT SP, this goes into nomans
                                land.                                        */
   asm   mov   sp, word ptr es:[bx+2];
   asm   mov word ptr es:[bx+4], RUNNING; /* PTN(process_state) = RUNNING;   */
   asm   inc word ptr es:[bx+16];         /* PTN(clock_ticks)++;             */
   outportb( PIC, EOI );                  /* signal EOI to 8239 PIC          */
}

void far newprocess( task_number, stack, fptr, msgbuffer )
int task_number;
unsigned int stack[];
void far (*fptr)();
char far *msgbuffer;
{
   /* inserts a task into PROCESS table, simulates an INT call on the top of
      its stack space                                                        */
   /* save SS:SP address of tasks stack space in PROCESS table               */
   PTN(SS) = (unsigned int) FP_SEG( stack );
   PTN(SP) = (unsigned int) FP_OFF( &stack[STACK_LEN -13]);
   PTN(process_state) = READY;
   PTN(process_priority) = NORMAL_PRIORITY;
   PTN(sleep_rate) = 0;
   PTN(msg) = FALSE;
   PTN(msgbuf) = msgbuffer;
   PTN(clock_ticks) = 0;
   stack[STACK_LEN - 2] = (unsigned int) 0x0200;           /* insert a dummy flag value, interrupts enabled */
   stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr );   /* place CS:IP of task into tasks stack space    */
   stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr );
   stack[STACK_LEN - 5] = _AX;
   stack[STACK_LEN - 6] = _BX;
   stack[STACK_LEN - 7] = _CX;
   stack[STACK_LEN - 8] = _DX;
   stack[STACK_LEN - 9] = _DS;   /* _ES */
   stack[STACK_LEN -10] = _DS;   /* _DS */
   stack[STACK_LEN -11] = _SI;
   stack[STACK_LEN -12] = _DI;
   stack[STACK_LEN -13] = _BP;
}

int far ctc( void )              /* ctrlc handler                            */
{
   disable();                    /* turn off interrupts                      */
   setvect(  0x08, oldtimer );   /* restore original timer                   */
   outportb( 0x43, 0x36 );       /* reset 8253 channel 0 to mode 3           */
   outportb( 0x40, 0x00 );
   outportb( 0x40, 0x00 );
   enable();                     /* reenable interrupts                      */
   exit( 0 );                    /* exit to DOS                              */
}

void far myexit( int exit_code )
{
   int i;
   disable();                    /* turn off interrupts                      */
   setvect(  0x08, oldtimer );   /* restore original timer                   */
   outportb( 0x43, 0x36 );       /* reset 8253 channel 0 to mode 3           */
   outportb( 0x40, 0x00 );
   outportb( 0x40, 0x00 );
   enable();                     /* reenable interrupts                      */
   for( i = 0; i < NUM_TASKS; i++ )
     printf("\nTask %d received %u clock_ticks", i, PROCESS[i].clock_ticks);
   exit( exit_code );
}

void far print( char *stringptr )
{
   while( *stringptr ) {
      offset = (x + ( y * 80 )) * 2;
      pokeb( 0xb800, offset, *stringptr );
      x++;
      if( x > 79 ) {
         x = 0; y++;
         if( y > 24 ) {
            y = 0; x = 0;
         }
      }
      stringptr++;
   }
}

void far task1( void )
{
   unsigned int loop;
   char far *msg;
   msg = farcalloc( 10, sizeof(char) );
   while( 1 ) {
      print("task1 ");
      for( loop = 0; loop < 65535; loop++ )
         ;
      msg[0] = '1';
      msg[1] = '\0';
      sendmessage(2, msg);
   }
}

void far task2( void )
{
   char far *msg;
   while( 1 ) {
      msg = recievemessage();
      print("Task2 woken by task1");
   }
}

void far main()
{
   unsigned char ch;
   unsigned int i;
   task_number=0;       /* current running task is main                      */
   x = 0;
   y = 0;

   /* install tasks in PROCESS                                               */
   newprocess( 0, stack_main,  &main,  msg_main  );
   newprocess( 1, stack_task1, &task1, msg_task1 );
   newprocess( 2, stack_task2, &task2, msg_task2 );

   setcbrk( 1 );                           /* enable for every system call   */
   ctrlbrk( ctc );                         /* redirect CTRL-C                */
   oldtimer = getvect( 0x08 );             /* save old RTC interrupt         */
   disable();                              /* stop interrupts                */
   setuptimer( transfer );                 /* hook scheduler into RTC        */
   enable();                               /* and now its away               */
   while( 1 ) {
      /* this portion of the code checks for F1 key press and if found
         restores machine state and exits to DOS
      */
      disable();
      if( kbhit() != 0 ) {
         ch = getch();
         if( ch == 00 ) {
            ch = getch();
	  if( ch == F1 ) myexit(0);
         }
         else ungetch(ch);
      }
      enable();

      /* main task portions now */
      print("main ");
      for( i = 0; i < 32000; i++ );
   }
}



LISTING 3  task3.c  a rudimentary DOS pre-emptive scheduler


/* task3.c, interrupt driven round robin scheduler, using DOS routines
   Copyright Brian Brown, CIT, 28th September 1989
   All rights reserved.

   To compile this program type,
tcc -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task3.c
*/

#include <stdio.h>
#include <dos.h>
#include <conio.h>

#define NUM_TASKS      3
#define STACK_LEN   4096
#define F1          0x3b
#define PIC         0x20
#define EOI         0x20

struct task {
   unsigned int SS;
   unsigned int SP;
} PROCESS[NUM_TASKS];

unsigned int stack_main[STACK_LEN];
unsigned int stack_task1[STACK_LEN];
unsigned int stack_task2[STACK_LEN];
int task_number, x, y, offset, ch;
void far interrupt (*oldtimer)();
void far interrupt (*oldint10)();
char far *INDOS;
unsigned int InVideoBios;

void interrupt far newint10( void )
{
   ++InVideoBios;
   (*oldint10)();
   --InVideoBios;
   outportb( PIC, EOI);
}

void far setuptimer( void interrupt far (*scheduler)() )
{
   /* link transfer into RTC interrupt 0x08, every 55ms but be careful, interrupt 8h, also drives time of day and
      disk drive motor control */
   setvect( 0x08, scheduler );
}

void far interrupt scheduler( void )
{
   /* interrupts have been disabled, trap disabled */
   if( *INDOS || InVideoBios ) {  /* don't switch if in DOS */
      outportb( PIC, EOI );
      return;                     /* go back to interrupted task */
   }
   PROCESS[task_number].SS = (unsigned int) _SS;   /* save current running task */
   PROCESS[task_number].SP = (unsigned int) _SP;
   task_number++;   /*set up for new task, switch stacks and restore registers for that task*/
   if( task_number > (NUM_TASKS-1) ) task_number = 0;
   _SS = PROCESS[task_number].SS;
   /* _SP = PROCESS[task_number].SP;   This statement must be done is assembler else the C Compiler
      will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes
      into nomans land. */
   asm   mov   sp, word ptr es:[bx+2];
   outportb( PIC, EOI );   /* signal EOI to 8239 PIC */
}

void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)())
{
   /* inserts a task into PROCESS table, and simulates an INT call on the top of its stack space */
   /* save SS:SP address of tasks stack space in PROCESS table */
   PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace );
   PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN -13]);
   stackspace[STACK_LEN - 2] = (unsigned int) 0x0200;           /* insert a dummy flag value, interrupts enabled */
   stackspace[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr );   /* place CS:IP of task into tasks stack space    */
   stackspace[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr );
   stackspace[STACK_LEN - 5] = _AX;
   stackspace[STACK_LEN - 6] = _BX;
   stackspace[STACK_LEN - 7] = _CX;
   stackspace[STACK_LEN - 8] = _DX;
   stackspace[STACK_LEN - 9] = _DS;   /* _ES */
   stackspace[STACK_LEN -10] = _DS;   /* _DS */
   stackspace[STACK_LEN -11] = _SI;
   stackspace[STACK_LEN -12] = _DI;
   stackspace[STACK_LEN -13] = _BP;
}

int far ctc( void )   /* ctrlc handler */
{
   disable();
   setvect( 0x08, oldtimer );
   enable();
   return 0;
}

void far task1( void )
{
   unsigned int z;
   int i;
   i = 0;
   while( 1 ) {
      printf("task1 = %d\n", i);
      i += 1;
      for( z = 1; z < 65000; z++ );
   }
}

void far task2( void )
{
   unsigned int z;
   int i;
   i = 0;
   while( 1 ) {
      printf("task2 = %d\n", i);
      i += 2;
      for( z = 1; z < 65000; z++ );
   }
}

void far main()
{
   unsigned int z;
   int i;
   i = 0;

   task_number=0;
   newprocess( 0, stack_main, &main );
   newprocess( 1, stack_task1, &task1 );
   newprocess( 2, stack_task2, &task2 );

   setcbrk( 1 );                 /* enable for every system call */
   ctrlbrk( ctc );               /* redirect CTRL-C        */
   _AH = 0x34;                   /* get pointer to INDOS flag */
   geninterrupt( 0x21 );
   INDOS = MK_FP( _ES, _BX );

   oldint10 = getvect( 0x10 );   /* set up new int10 video */
   setvect( 0x10, newint10 );
   InVideoBios = 0;

   oldtimer = getvect( 0x08 );   /* save old timer interrupt */
   disable();
   setuptimer( scheduler );      /* place scheduler into RTC */
   enable();                     /* and start it going */
   while( 1 ) {
      disable();      /* this portion allows resetting and exitting back to DOS */
      if( kbhit() != 0 ) {        /* check for keypress */
         ch = getch();
         if( ch == 00 ) {        /* is it a function key? */
            ch = getch();
            if( ch == F1 ) {    /* yes, then exit program */
               disable();
               setvect( 0x08, oldtimer );
               setvect( 0x10, oldint10 );
               clrscr();
               enable();
               exit(0);
            }
         }
         else ungetch(ch);        /* if not a function key, put back */
      }
      /* this is main task portion */
      enable();
      printf("main = %d\n", i );
      i += 3;
      for( z = 1; z < 65000; z++ );
   }
}



Listing 4   An embedded application for PC/XT using DIO card/panel


CROM.H
/* CROM.H, designed by SE2, 1990 for EMBEDDED CODE */
struct eightbit
{
   unsigned char al, ah, bl, bh, cl, ch, dl, dh;
};

struct sixteenbit
{
   unsigned int ax, bx, cx, dx, si, di, cflag;
};

union REGS
{
   struct sixteenbit x;
   struct eightbit   h;
};

/* function prototypes follow */
extern void outportb( unsigned int, char);
extern char inportb( unsigned int );
extern void int86( int, union REGS *, union REGS * );
extern void enable( void );
extern void disable( void );
extern void setvect( int interruptnumber, void interrupt (*isr)() );



TCSTART.ASM
; tcstart.asm

extrn  _main:far

_text		segment byte	public 'CODE'
_text		ends
_textend		segment	para	public 'CODEEND'
_textend		ends
_data		segment para	public 'DATA'
_data		ends
_dataend		segment para public 'DATAEND'
_dataend		ends
_bss		segment para	public 'BSS'
_bss		ends
_bssend		segment byte	public 'BSSEND'
_bssend		ends
_stack		segment para	stack  'STACK'
_stack		ends

DGROUP		group	_DATA, _DATAEND, _BSS, _BSSEND
CGROUP		group	_TEXT, _TEXTEND

_TEXT		segment
	assume CS:CGROUP, DS:DGROUP, ES:DGROUP, SS:_STACK
public	start
	db	55h
	db	0AAh
	db	40h
	jmp	start

start:	cli	           ; disable interrupts
	mov	ax, _STACK ; initialise stack
	mov	ss, ax
	mov	ax, offset stackend
	mov	sp, ax
	mov	ax, seg _BSS
	mov	es, ax
	mov	cx, offset DGROUP:endbss
	mov	di, 0
	mov	ax, 0
	rep	stosb	; write to ES:DI

	mov	ax, seg DGROUP
	mov	es, ax	; point ES to _DATA
	mov	cx, offset DGROUP:enddata
	mov	si, 0
	mov	di, 0
	assume ds:CGROUP:_TEXTEND
	mov	ax, seg _TEXTEND:codeend
	inc	ax
	mov	ds, ax	; point DS to _CONST
	rep	movsb	; copy _CONST to _DATA
	push	es	; point DS to _DATA
	pop	ds
	mov	al, 80h      ; enable NMI
	out	0a0h, al
	mov	al, 0bch     ; enable 8239 PIC  1011-1100 (irq0,1,6 enabled)
	out	21h, al
	sti		     ; enable interrupts
	call	_main
	jmp	start
_TEXT	ends

_TEXTEND	segment
	public	codeend
	db  16  dup ( ? )
codeend	label	byte
_TEXTEND	ends

_STACK	segment
	db	1024 dup ('STACK');
stackend	label	word
_STACK	ends

_BSSEND	segment
	public	endbss
endbss	label	byte
_BSSEND	ends

_DATAEND	segment
	public enddata
enddata	label	byte
_DATAEND	ends

        end



TCLIB.ASM
; TCLIB.ASM, library routine for int86() calls from EMBEDDED CODE

public	_inportb
public	_outportb
public	_enable
public	_disable
public	_setvect

CODE	segment	para PUBLIC 'CODE'
	assume	cs:CODE
	name	tclib

_enable    proc    far
           sti
           ret
_enable    endp

_disable   proc  far
           cli
           ret
_disable   endp

_setvect   proc far
           push	bp              ;acess local vars inside functions
           mov	bp,sp           ;use bp to acess passed parameters
           push ax
           push bx
           push dx
           push es
           mov  al, byte ptr [bp+6]  ; int number
           mov  ah, 0
           rol  ax, 1
           rol  ax, 1
           mov  bx, ax
           xor  ax, ax
           mov  es, ax
           mov  dx, [bp+10]           ; segment
           mov  es:[bx+2], dx
           mov  dx, [bp+8]           ; offset
           mov  es:[bx], dx
           pop  es
           pop  dx
           pop  bx
           pop  ax
           pop  bp
           ret
_setvect   endp

_inportb	proc	far
	push	bp              ;acess local vars inside functions
	mov	bp,sp           ;use bp to acess passed parameters
	push	dx
	mov	dx,[bp+6]      ; get port address
	xor	ax,ax           ; clear ax
	in	al, dx          ; read from port into al register
	mov	ah, 00h         ; ensure high byte of ax is cleared
	pop	dx
	pop	bp
	ret
_inportb	endp

_outportb   proc	far
	push	bp              ;acess local vars inside functions
	mov	bp,sp           ;use bp to acess passed parameters
	push	dx
	mov	dx,[bp+6]      ;get port address
	mov	al, byte ptr [bp+8]
	out	dx, al
	pop	dx
	pop	bp
	ret
_outportb	endp

CODE	ends
	end


TSKPCIO.C
/* task.c, interrupt driven round robin scheduler
   Copyright Brian Brown, CIT, 28th September 1989
   All rights reserved.
   requires DIO card
   embedded system using multi-tasking
*/

#include "crom.h"
#define FP_OFF(fp)	((unsigned)(fp))
#define FP_SEG(fp)	((unsigned)((unsigned long)(fp) >> 16))
#define TRUE             1
#define FALSE            0
#define NUM_TASKS        3              /* number of tasks in system 0,1,2   */
#define READY            0              /* waiting for the processor         */
#define RUNNING          1              /* currently has the processor       */
#define STACK_LEN       1025            /* length of stack for a task        */
#define PIC             0x20            /* Priority Interrupt controller     */
#define EOI             0x20            /* end of interrupt for PIC          */
#define PTN(x)          PROCESS[task_number].x

struct task {                           /* process control block for tasks   */
   unsigned int SS;                     /* stack segment register            */
   unsigned int SP;                     /* stack pointer for each task       */
   unsigned int process_state;          /* task state                        */
} PROCESS[NUM_TASKS];                   /* one entry per known task          */

extern unsigned _stklen = 8192;

unsigned int stack_main[STACK_LEN];     /* stack for main task               */
unsigned int stack_task1[STACK_LEN];    /* stack for task1                   */
unsigned int stack_task2[STACK_LEN];    /* stack for task2                   */

unsigned int task_number;               /* holds current running task ID     */
void far newprocess( int task_number, unsigned int stack[], void far (*fptr)() );

void far setuptimer( void interrupt far (*switcher)() )
{
   setvect(  0x08, switcher );          /* link into RTC interrupt 0x08      */
   outportb( 0x43, 0x34 );              /* reprogram 8253, mode 2 channel 0  */
   outportb( 0x40, 0x00 );
   outportb( 0x40, 0x40 );
}

void far interrupt transfer( void )
{
   PTN(SS) = (unsigned int) _SS;  /* save current running task stack pointers*/
   PTN(SP) = (unsigned int) _SP;
   PTN(process_state) = READY;

   task_number++;                       /* select next task to run           */
   if( task_number > (NUM_TASKS-1) )  task_number = 0;
   _SS = PTN(SS);               /* swap to stack of newly selected task      */
   /* _SP = PTN(SP);            This statement must be done is assembler else
                                the C Compiler will try to use the stack to
                                generate a valid _ES reference. As SS has been
                                adjusted, BUT NOT SP, this goes into nomans
                                land.                                        */
   asm   mov   sp, word ptr es:[bx+2];
   PTN(process_state) = RUNNING;
   outportb( PIC, EOI );                  /* signal EOI to 8239 PIC          */
}

void far newprocess( task_number, stack, fptr )
int task_number;
unsigned int stack[];
void far (*fptr)();
{
   /* inserts a task into PROCESS table, simulates an INT call on the top of
      its stack space                                                        */
   /* save SS:SP address of tasks stack space in PROCESS table               */
   PTN(SS) = (unsigned int) FP_SEG( stack );
   PTN(SP) = (unsigned int) FP_OFF( &stack[STACK_LEN -13]);
   PTN(process_state) = READY;
   /* insert a dummy flag value, interrupts enabled                          */
   stack[STACK_LEN - 2] = (unsigned int) 0x0200;
   /* place CS:IP of task into tasks stack space                             */
   stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr );
   stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr );
   stack[STACK_LEN - 5] = _AX;
   stack[STACK_LEN - 6] = _BX;
   stack[STACK_LEN - 7] = _CX;
   stack[STACK_LEN - 8] = _DX;
   stack[STACK_LEN - 9] = _DS;   /* _ES */
   stack[STACK_LEN -10] = _DS;   /* _DS */
   stack[STACK_LEN -11] = _SI;
   stack[STACK_LEN -12] = _DI;
   stack[STACK_LEN -13] = _BP;
}

/* reads switches, transfers to seven segment */
void far task1( void )
{
   unsigned char sample;

   while( 1 ) {
        sample = inportb(0x225);
        outportb( 0x224, sample );
   }
}

/* rotates leds */
void far task2( void )
{
   unsigned char ledbyte;
   unsigned int delay;

   ledbyte = 1;
   while( 1 ) {
        outportb( 0x223, ledbyte );
        ledbyte = (ledbyte << 1) & 0xfe;
        if( (ledbyte & 0xff) == 0 )
            ledbyte = 1;
        for( delay = 1; delay < (unsigned int) 65000; delay++ )
             ;
   }
}

void far main()
{
   task_number=0;       /* current running task is main                      */

   /* install tasks in PROCESS                                               */
   newprocess( 0, stack_main,  main  );
   newprocess( 1, stack_task1, task1 );
   newprocess( 2, stack_task2, task2 );

   outportb(0x227, 0x83);                  /* initialise DIO card            */
   outportb(0x223, 0x00);                  /* turn off all leds              */

   disable();                              /* stop interrupts                */
   setuptimer( transfer );                 /* hook scheduler into RTC        */
   enable();                               /* and now its away               */
   for( ; ; )
   {
      ;
   }
}



TSKPCIO.CFG
dup DATA CONST
class CODE = 0xd000
class STACK = 0x1000
class DATA = 0x2000
order DATA DATAEND BSS BSSEND
order CODE CODEEND CONST
rom CODE CONST



DOIT.BAT
tasm /mx tcstart.asm
tasm /mx tclib.asm
tcc -a- -c -f- -G- -K -B -ml -M -N- -O- -r- -v- -y- -Z- -S tskpcio.c
tlink /m tcstart tskpcio tclib, tskpcio, tskpcio
locate tskpcio
hexbin2 tskpcio.hex tskpcio.bin i d000



home


© Copyright Brian Brown, 1988-2000. All rights reserved.